import * as React from "react"
const h = React.createElement

export interface DispatchAction<T extends object, F = unknown> {
    (action: { (state: T): T }, props?: F): void
}
export interface Middleware<T extends object, F = unknown> {
    (state: T, nextState: T, props?: F): void
}

export const isSameObject = (obj1: object, obj2: object) => {
    let keys1 = Object.keys(obj1)
    let keys2 = Object.keys(obj2)
    if (keys1.length !== keys2.length) {
        return false
    } else if (keys1.length + keys2.length === 0) {
        return true
    }
    return keys1.every(k => obj1[k] === obj2[k])
}

export const createStore = function <T extends object, F = never>(middlewares?: Middleware<T, F>[]) {

    const fn = function (initState: T) {

        let store = initState
        let updateQueue: Function[] = []
    
        const connect = function <O extends object> (
            mapProps?: (props?: O) => O,
            mapDispatch?: (props?: O) => O
        ) {
            return function <P extends object> (Com: React.ComponentType<P & Partial<O>>): React.ComponentType<Omit<P & Partial<O>, keyof O> & Partial<O>> {
                class ComP extends React.Component<P & O> {
                    declare props: P & O
                    tempProps: P
                    tempUpdate: () => void
                    execProps() {
                        const { props } = this
                        const res1 = mapProps(props)
                        let res2 = {}
                        if (mapDispatch) {
                            res2 = mapDispatch(Object.assign({}, props, res1))
                        }
                        return Object.assign({}, props, res1, res2)
                    }
                    constructor(props: P & O) {
                        super(props)
                        let t = this
                        t.execProps = t.execProps.bind(t)
                        t.tempProps = t.execProps()
                        t.tempUpdate = function () {
                            let newProps = t.execProps()
                            if (!isSameObject(t.tempProps, newProps)) {
                                t.tempProps = newProps
                                t.forceUpdate && t.forceUpdate()
                            }
                        }
                        updateQueue.push(t.tempUpdate)
                    }
                    componentWillUnmount() {
                        updateQueue.splice(updateQueue.indexOf(this.tempUpdate), 1)
                    }
                    render() {
                        return <Com {...this.tempProps} {...this.props}/>
                    }
                }

                return ComP
            }
        }

        const dispatch: DispatchAction<T, F> = (action, props) => {
            let res = action(store)
            middlewares && middlewares.map(middleware => middleware(store, res, props))
            if (res !== store) {
                store = res
                updateQueue.map(f => f())
            }
        }
        return {
            connect,
            getState: () => store,
            dispatch,
        }
    }

    return fn
}

export type IPreact<T extends object, F = unknown> = ReturnType<ReturnType<typeof createStore<T, F>>>
export type Connect = IPreact<never>['connect']

export default createStore